package edu.cs.usask.ca.silver.plugin.rcp.views.projectviewer;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.UUID;

import org.apache.log4j.Logger;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.NotEnabledException;
import org.eclipse.core.commands.NotHandledException;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.dialogs.FilteredTree;
import org.eclipse.ui.dialogs.PatternFilter;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.services.ISourceProviderService;

import edu.cs.usask.ca.silver.api.persistence.StorageI;
import edu.cs.usask.ca.silver.businesslogic.ids.ModelVersionID;
import edu.cs.usask.ca.silver.businesslogic.ids.ScenarioCollectionID;
import edu.cs.usask.ca.silver.businesslogic.ids.ScenarioID;
import edu.cs.usask.ca.silver.businesslogic.ids.UserID;
import edu.cs.usask.ca.silver.exceptions.UnableToInitializeStorageException;
import edu.cs.usask.ca.silver.plugin.rcp.components.SilverNameEnums;
import edu.cs.usask.ca.silver.plugin.rcp.components.SilverTreeObject;
import edu.cs.usask.ca.silver.plugin.rcp.dao.SilverBusinessLogicController;
import edu.cs.usask.ca.silver.plugin.rcp.jobs.LoggerReportScheduler;
import edu.cs.usask.ca.silver.plugin.rcp.sourceprovider.SilverUISourceProvider;

public abstract class AbstractProjectView extends ViewPart implements
		ProjectViewI {

	protected TreeViewer treeViewer;
	protected SilverTreeObject silverTreeRoot;
	protected FilteredTree tree;
	protected ArrayList<IPropertyChangeListener> listeners;
	protected StorageI storage;
	protected UserID userid;

	// only to be modified by invalidate() and validate()
	private boolean isValid;
	private static Logger logger = Logger.getLogger(AbstractProjectView.class);

	/**
	 * This is a callback that will allow us to create the viewer and initialize
	 * it.
	 */
	@Override
	public final void createPartControl(Composite parent) {
		initializeAssociatedStorageAndTreeRootObject();
		// Filter
		PatternFilter filter = new PatternFilter();
		this.tree = new FilteredTree(parent, SWT.MULTI | SWT.H_SCROLL
				| SWT.V_SCROLL | SWT.BORDER | SWT.VIRTUAL, filter, true);
		this.treeViewer = tree.getViewer();
		// Although may use more memory, faster with large amount of domain objects.
		this.treeViewer.setUseHashlookup(true);

		this.getSite().setSelectionProvider(this.treeViewer);

		if (this.storage != null && this.silverTreeRoot != null) {
			this.treeViewer.setLabelProvider(new ProjectViewLabelProvider(
					this.storage));
			this.treeViewer.setContentProvider(new ProjectViewContentProvider(
					this.storage, this.silverTreeRoot));
			this.treeViewer.setInput(silverTreeRoot);
			this.treeViewer.refresh();
			isValid = true;
		}

		listeners = new ArrayList<IPropertyChangeListener>();

		addSelectionChangedListener();
		registerPopUpMenuHandler();
		this.treeViewer
				.addDoubleClickListener(new ProjectViewDoubleClickListener(
						this, this.getSite().getWorkbenchWindow()
								.getActivePage()));
		this.setStatusLine(getTitleImage(), "status line");

		this.addDragAndDropListener();

	}

	protected void startMonitorDatabaseJob(final String viewId,
			final int seconds) {
		final long sleep = seconds * 1000;

		Job job = new Job(viewId + "DatabaseMonitorJob") {
			@Override
			protected IStatus run(IProgressMonitor monitor) {

				setPriority(DECORATE);

				if (storage != null) {
					try {
						if (storage.pingConnection()) {
							validate();
						} else {
							invalidate();
							
						}
					} catch (SQLException se) {
						LoggerReportScheduler
								.scheduleInfoLogging(
										logger,
										"database montitor detected a broken connection, invalidating the projectview: "
												+ viewId);
						invalidate();
					} finally {
						this.schedule(sleep);
					}
				}
				// reschedule based on seconds given
				this.schedule(sleep);
				return Status.OK_STATUS;
			}
		};
		job.schedule();
	}

	public final void invalidate() throws NullPointerException {
		// do nothing if already not valid
		if (!this.isValid)
			return;
		if (this.tree == null || this.treeViewer == null)
			return;

		final TreeViewer treeView = this.treeViewer;
		Display.getDefault().asyncExec(new Runnable() {
			public void run() {
				LoggerReportScheduler.scheduleInfoLogging(logger,
						"invalidating");
				treeView.getTree().setEnabled(false);
			}
		});

		this.isValid = false;
	}

	public final void validate() throws NullPointerException {
		// do nothing if already valid
		if (this.isValid)
			return;
		if (this.tree == null || this.treeViewer == null)
			return;

		final TreeViewer treeView = this.treeViewer;
		Display.getDefault().asyncExec(new Runnable() {
			public void run() {
				LoggerReportScheduler.scheduleInfoLogging(logger, "validating");
				treeView.getTree().setEnabled(true);
			}
		});

		this.isValid = true;
	}

	// ////////////////////////////////////////
	// / DND in TreeViewer
	// / Currently only works with Local Derby
	// //////////////////////////////////////////
	private void addDragAndDropListener() {

		// per discussion the only function available to ProjectViewer is COPY

		// DND.DROP_MOVE stands for regular left mouse button drag and drop
		// DND.DROP_COPY stands for special drag and drop envolving 'option' or
		// 'alt'
		int ops = DND.DROP_MOVE | DND.DROP_COPY;

		// Only TextTransfer worked in the experiment, yet this saves time to
		// implement a specific Transfer class
		Transfer[] transfers = new Transfer[] { TextTransfer.getInstance() };

		// initialize dragsource for treeViewer
		final DragSource source = new DragSource(treeViewer.getTree(), ops);

		// specific buffer for IStructuredSelections
		final IStructuredSelection[] dragSelection = new IStructuredSelection[1];

		// set TextTransfer to drag source
		source.setTransfer(transfers);
		source.addDragListener(new DragSourceListener() {

			public void dragFinished(DragSourceEvent event) {
				// anything to do after finished drag
				logger.info("drag finished");
			}

			public void dragSetData(DragSourceEvent event) {

				IStructuredSelection selection = dragSelection[0];
				Object[] cast = selection.toArray();
				SilverTreeObject tempTreeObj;
				String buffer = " ";

				/*
				 * Verify all selection are of type ScenarioID, ignore the
				 * selections that are not. then append id to string buffer for
				 * transfer
				 * 
				 * The following methods can not be separated to an independent
				 * method.
				 */
				for (int i = 0; i < cast.length; i++) {
					tempTreeObj = (SilverTreeObject) cast[i];

					if (tempTreeObj.getSilverObjectId() instanceof ScenarioID) {
						buffer = buffer
								+ ":s*"
								+ ((ScenarioID) tempTreeObj.getSilverObjectId())
										.getUUIdVal().toString();
					} else if (tempTreeObj.getSilverObjectId() instanceof ScenarioCollectionID) {
						buffer = buffer
								+ ":sc*"
								+ ((ScenarioCollectionID) tempTreeObj
										.getSilverObjectId()).getUUIdVal()
										.toString();
					}

				}

				// now the buffer is ready, appoint event.data to buffer.
				event.data = buffer;

			}

			// Check and Store selection at the beginning of drag
			public void dragStart(DragSourceEvent event) {
				IStructuredSelection selection = (IStructuredSelection) treeViewer
						.getSelection();

				if (selection.isEmpty()) {
					event.doit = false;
				} else {
					dragSelection[0] = selection;
					event.doit = true;
				}
			};

		});

		// Instantiate DropTarget for treeViewer
		DropTarget target = new DropTarget(treeViewer.getTree(), ops);

		// set the same transfer to drop target
		target.setTransfer(transfers);
		target.addDropListener(new DropTargetAdapter() {

			public void dragOver(DropTargetEvent event) {
				event.feedback = DND.FEEDBACK_EXPAND | DND.FEEDBACK_SCROLL;
			}

			public void drop(DropTargetEvent event) {
				if (event.data == null) {
					event.detail = DND.DROP_NONE;
					return;
				}

				// cast the drop target in the treeView to SilverTreeObject for
				// method call
				SilverTreeObject dropEntity = ((SilverTreeObject) (event.item
						.getData()));

				String dropIds = (String) event.data;

				// regular expression to chop the chunk of String into an array
				// of ids
				// aparentely the following code can not be separated into
				// another methods.
				String[] choppedIds = dropIds.split(":");
				try {
					// refresh the drop target entity in treeViewer after drop
					// action
					treeViewer.refresh(dropEntity);
					if (choppedIds.length == 0)
						return;

					if (dropEntity.getSilverObjectId() instanceof ScenarioCollectionID) {
						ScenarioID tempSId = null;
						ScenarioCollectionID tempScId = (ScenarioCollectionID) dropEntity
								.getSilverObjectId();
						for (int j = 0; j < choppedIds.length; j++) {
							if (choppedIds[j].startsWith("s*")) {
								tempSId = new ScenarioID(UUID
										.fromString(choppedIds[j].substring(2)));
								SilverBusinessLogicController.borrowScenario()
										.duplicateScenario(tempSId, tempScId,
												getAssociatedStorage());

							}
						}
						SilverBusinessLogicController.returnAllScenarioObject();

					} else if (dropEntity.getSilverObjectId() instanceof ModelVersionID) {
						ScenarioCollectionID tempScId = null;
						ScenarioCollectionID dupScId = null;
						ModelVersionID tempMvId = (ModelVersionID) dropEntity
								.getSilverObjectId();
						for (int j = 0; j < choppedIds.length; j++) {
							if (choppedIds[j].startsWith("sc*")) {

								tempScId = new ScenarioCollectionID(UUID
										.fromString(choppedIds[j].substring(3)));

								dupScId = SilverBusinessLogicController
										.borrowMvObject()
										.duplicateScenarioCollection(tempScId,
												tempMvId,
												getAssociatedStorage());

							}
						}
						SilverBusinessLogicController.returnAllMvObject();
						SilverBusinessLogicController.returnAllScenarioObject();
						SilverBusinessLogicController.returnAllScObject();
					}
				} catch (Exception e) {
					LoggerReportScheduler.scheduleErrorLogging(logger,
							"Error in AbstractProjectView", e);
				}
				treeViewer.refresh(dropEntity);

			}

			// TODO: need better and testable code refactoring
		});

		// end of DND in TreeViewer
		// ////////////////////////////////////////

	}

	// ////////////////////////////////
	// abstract
	// //////////////////////////////
	/**
	 * private method to set up the tree viewer structure for the project viewer
	 * this method will attempt to initialize database.
	 * 
	 */
	protected abstract void initializeAssociatedStorageAndTreeRootObject();

	public void resetAssociatedStorage(StorageI databaseInstance) {
		if (databaseInstance == null)
			return;

		this.storage = databaseInstance;

		try {
			this.storage.initializeStorage();
			this.userid = SilverBusinessLogicController
					.fetchUserIDfromDatabaseByPreferenceInfo(this.storage);
			this.silverTreeRoot = new SilverTreeObject(this.userid);
		} catch (Exception e) {
			LoggerReportScheduler.scheduleErrorLogging(logger,
					"Error in AbstractProjectView", e);
		} finally {

			this.treeViewer = tree.getViewer();
			this.getSite().setSelectionProvider(this.treeViewer);
			this.treeViewer.setLabelProvider(new ProjectViewLabelProvider(
					this.storage));
			this.treeViewer.setContentProvider(new ProjectViewContentProvider(
					this.storage, this.silverTreeRoot));
			this.treeViewer.setInput(this.silverTreeRoot);
			this.treeViewer.refresh();
		}
	}

	public void resetAssociatedUserToPreferenceInformation() {
		try {
			this.storage.initializeStorage();
			this.userid = SilverBusinessLogicController
					.fetchUserIDfromDatabaseByPreferenceInfo(this.storage);
			this.silverTreeRoot = new SilverTreeObject(this.userid);
		} catch (Exception e) {
			LoggerReportScheduler.scheduleErrorLogging(logger,
					"Error in AbstractProjectView", e);
		} finally {

			this.treeViewer = tree.getViewer();
			this.getSite().setSelectionProvider(this.treeViewer);
			this.treeViewer.setLabelProvider(new ProjectViewLabelProvider(
					this.storage));
			this.treeViewer.setContentProvider(new ProjectViewContentProvider(
					this.storage, this.silverTreeRoot));
			this.treeViewer.setInput(this.silverTreeRoot);
			this.treeViewer.refresh();
		}

	}

	protected boolean checkIfUserPreferenceInfoIsAvailable() {
		if (SilverBusinessLogicController
				.checkIfHasUserInformationInPreference())
			return true;

		// no user info is in preference, register user.
		IHandlerService handlerService = (IHandlerService) getSite()
				.getWorkbenchWindow().getWorkbench()
				.getService(IHandlerService.class);
		try {
			handlerService
					.executeCommand(
							"edu.cs.usask.ca.silver.plugin.rcp.handlers.CreateUserCommand",
							null);
		} catch (ExecutionException e) {
			LoggerReportScheduler.scheduleErrorLogging(logger,
					"Error in AbstractProjectView", e);
		} catch (NotDefinedException e) {
			LoggerReportScheduler.scheduleErrorLogging(logger,
					"Error in AbstractProjectView", e);
		} catch (NotEnabledException e) {
			LoggerReportScheduler.scheduleErrorLogging(logger,
					"Error in AbstractProjectView", e);
		} catch (NotHandledException e) {
			LoggerReportScheduler.scheduleErrorLogging(logger,
					"Error in AbstractProjectView", e);
		}

		return true;
	}

	public final StorageI getAssociatedStorage() {
		return this.storage;
	}

	public final UserID getAssociatedUserID() {
		return this.userid;
	}

	private void registerPopUpMenuHandler() {
		// First we create a menu Manager
		MenuManager menuManager = new MenuManager();
		Menu menu = menuManager.createContextMenu(this.getTreeViewer()
				.getControl());
		// Set the MenuManager
		this.getTreeViewer().getControl().setMenu(menu);
		getSite().registerContextMenu(menuManager, this.getTreeViewer());
		// Make the selection available
		getSite().setSelectionProvider(this.getTreeViewer());
	}

	public void setStatusLine(Image image, String message) {
		// Get the status line and set the text
		IActionBars bars = getViewSite().getActionBars();
		bars.getStatusLineManager().setMessage(image, message);
	}

	/*
	 * reset tree root and repaint the entire project tree.
	 */
	public void resetTreeRoot(SilverTreeObject silverTreeRoot) {
		this.treeViewer.setInput(silverTreeRoot);
		this.treeViewer.refresh();
	}

	protected void setTreeViewer() {

	}

	private void addSelectionChangedListener() {
		this.treeViewer
				.addSelectionChangedListener(new ISelectionChangedListener() {

					@Override
					public void selectionChanged(SelectionChangedEvent event) {
						// update SELECTED_STATE in SilverUISourceProvider
						if (event.getSelection().isEmpty()) {
							return;
						}

						IStructuredSelection selection = (IStructuredSelection) event
								.getSelection();

						SilverTreeObject tempObject = (SilverTreeObject) selection
								.getFirstElement();

						// Access SilverUISourceProvider and update the
						// selection
						ISourceProviderService sourceProviderService = (ISourceProviderService) getSite()
								.getService(ISourceProviderService.class);
						SilverUISourceProvider sourceProvider = (SilverUISourceProvider) sourceProviderService
								.getSourceProvider(SilverUISourceProvider.PROJECTVIEW_SELECTED_STATE);

						sourceProvider.saveProjectViewSelection(tempObject
								.getSilverObjectId());

					}

				});
	}

	/**
	 * 
	 * 
	 * @return SilverTreeObject the root object
	 */
	public SilverTreeObject getSilverTreeRoot() {
		return silverTreeRoot;
	}

	/**
	 * Passing the focus request to the viewer's control.
	 */
	public void setFocus() {
		this.treeViewer.getControl().setFocus();
	}

	/**
	 * updates the particular object that is being updated
	 * 
	 * TODO Only tested with creating new scenario, more tests regarding
	 * MVersion and MProject is necessary.
	 * 
	 * @param modelObjectID
	 *            tree branch to be update
	 */
	public void updateTree() {
		IStructuredSelection selection = (IStructuredSelection) this.treeViewer
				.getSelection();
		SilverTreeObject tempObject = (SilverTreeObject) selection
				.getFirstElement();
		this.treeViewer.setExpandedState(tempObject, true);
		this.treeViewer.refresh(tempObject, false);
	}

	/**
	 * Refresh the tree.
	 */
	public void refresh() {
		this.treeViewer.refresh();
	}

	@Override
	public void dispose() {
		super.dispose();
	}

	/**
	 * Getter method for the static tree viewer.
	 * 
	 * @return treeviewer
	 */
	public TreeViewer getTreeViewer() {
		return this.treeViewer;
	}

	@Override
	public void propertyChange(PropertyChangeEvent event) {
		String val = (String) event.getNewValue();
		this.treeViewer.update(val, null);
	}

	/**
	 * A public method that allows listener registration
	 */
	public void addPropertyChangeListener(IPropertyChangeListener listener) {
		if (!listeners.contains(listener))
			listeners.add(listener);
	}

	/**
	 * A public method that allows listener registration
	 */
	public void removePropertyChangeListener(IPropertyChangeListener listener) {
		listeners.remove(listener);
	}

	public final SilverTreeObject getTreeSelectionAsSilverTreeObject() {
		ISelection selection = getTreeViewer().getSelection();

		IStructuredSelection iSelect = (IStructuredSelection) selection;
		SilverTreeObject silverTreeObject = (SilverTreeObject) (iSelect
				.getFirstElement());
		return silverTreeObject;
	}

	public final IStructuredSelection getTreeSelection() {
		return (IStructuredSelection) getTreeViewer().getSelection();
	}

	public final boolean checkIfSelectedIsOfSpecifiedType(SilverNameEnums type)
			throws ClassNotFoundException {
		Class<?> typeForMatch = Class
				.forName("edu.cs.usask.ca.silver.businesslogic.ids."
						+ type.getDisplayName());// throws
													// exception
		ISelection selection = getTreeViewer().getSelection();
		IStructuredSelection iSelect = (IStructuredSelection) selection;
		SilverTreeObject silverTreeObject = getTreeSelectionAsSilverTreeObject();

		if ((iSelect.size() == 1)
				&& (iSelect.getFirstElement() instanceof SilverTreeObject)
				&& (silverTreeObject.getSilverObjectId().getClass()
						.equals(typeForMatch))) {
			return true;

		}

		return false;
	}

}
